iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 9
0
Modern Web

Angular新手村學習筆記(2019)系列 第 9

Day09_Form Part II - Reactive Form

  • 分享至 

  • xImage
  •  

延申閱讀
ng conf 2019
The Forms Awakens | Sander Elias
https://www.youtube.com/watch?v=JCjyjdlaoaI&list=PLOETEcp3DkCpimylVKTDe968yNmNIajlR&index=23
https://github.com/SanderElias/ngObservableForm
Reactive Forms Demistified | Sani Yusuf & Katerina Skroumpelou
https://www.youtube.com/watch?v=Rq4vjSkidPk&list=PLOETEcp3DkCpimylVKTDe968yNmNIajlR&index=34

[S06E08] Form Part II - Reactive Form

https://www.youtube.com/watch?v=-0xwlICnq4w&list=PL9LUW6O9WZqgUMHwDsKQf3prtqVvjGZ6S&index=5

Reactive Form 推薦使用
與Template Drive Form一樣的是:有Form Control、Form Group
多一個Form Array

首先,先開好片頭導讀的文章,再跟著影片一起看,以後才有能力自己查文件

  1. 開啟 https://angular.io
  2. FUNDAMENTALS / Forms / Reactive Forms
    https://angular.io/guide/reactive-forms

起手式

  1. app.module.ts
    在app.module.ts裡import ReactiveFormsModule
import { ReactiveFormsModule } from '@angular/forms'; // <==

@NgModule({
  imports: [
    ReactiveFormsModule // <==
  ],
})
export class AppModule { }
  1. app.component.ts
    就可以在在component裡使用Reactive Form
import { Component } from '@angular/core';
import { FormGroup, FormControl, FormArray } from '@angular/forms';

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
    formData = new FormGroup({      // FormGroup:一個表單
        name: new FormControl(),    // FormControl:表單裡的control
        itemsFormControls: new FormArray([
            new FormControl(),
            new FormControl(),
            new FormControl()
        ]),
        itemsFormGroups: new FormArray([
            new FormGroup({ address: new FormControl() }),
            new FormGroup({ address: new FormControl() }),
            new FormGroup({ address: new FormControl() })
        ]),
    });
    // FormGroup()接 controls: { [key:string] : AbstractControl }
    name = new FormControl('');
    
    get itemsFormControls(){
        return this.formData.get('itemsFormControls') as FormArray;
    }
    get itemsFormGroups(){
        return this.formData.get('itemsFormGroups') as FormArray;
    }
    // 移除FormArray裡面的某一個control
    remove(idx){
        this.itemsFormGroups.removeAt(idx);
        //   ^^^^^^^^^這是FormArray    ^^^要移除的index
    }
    add(){
        this.itemsFormGroups.push(
            new FormGroup({address:new FormControl('手動新增的')})
            );
    }
    insert(idx){
        this.itemsFormGroups.insert(
            idx+1, // 插入的位置
            new FormGroup({address:new FormControl('手動插入的')})
            );
    }
    // 全部清空只留一筆
    clear(){
        while (this.itemsFormGroups.length >1){
            this.itemsFormGroups.removeAt(1);
        }
    }
    // 清空controls裡的值,或重設為某值
    reset(){
        this.items.reset(); // 如果都沒給,controls的值會變null
        this.items.reset([{address: '111'}]); // 裡面放array
    }
    
    resetFormData(){
        this.formData.reset({});
    }
}
  1. app.component.html
                    vvvvvv 在ts裡 formData = new FormGroup()
<form [formGroup]="formData">
        ^^^^^^^ 類似Template Form的ngForm
    <label>Name</label>
    <input type="text" /> 
    與Template Form不同的是,ReactiveFormsModule不用加name="xxx" ngModel
    而且設定formControlName="name"
    
    FormArray 範例
    1. formArray裡面放controls
    <div formArrayName="itemsFormControls">
                        ^^^^^ 此items非下面的items,而是還要再ts裡寫一個get items()
                              可以減少在HTML裡的code
        <div *ngFor="let item of itemsFormControls.controls; let i=index">
                                  ^^^^^^^^^^^ FormArray裡面每一個Object
            <input type="text" [formControlName]="i"/>
        </div>
    </div>
    
    2. formArray裡面放formGroups
    <div formArrayName="itemsFormGroups">
        <div *ngFor="let item of itemsFormGroups.controls; let i=index"   
                                               [formGroupName]="i">
                                               ^^^^^^^^^^^^^^^^^^^
            <input type="text" [formControlName]="address"/>
                                                   ^^^^^
            接在input後面
            <button (click)="remove(i)">X</button>
            <!-- this.itemsFormGroups.removeAt(idx); -->
                      ^^^^^^^^^^^^^^^這是formArray      
            <button (click)="insert(i)">+</button>
        </div>
        <button (click)="add()">ADD(push在最後)</button>
        <button (click)="clear()">用while清到只餘一筆</button>
        <button (click)="reset()">reset controls裡的值</button>
    </div>
    
</form>

{{ formData.value | json }}

操作FormArray裡面的controls

文件:
https://angular.io/api/forms/FormArray
https://angular.io/api/forms/FormArray#adding-or-removing-controls-from-a-form-array
To change the controls in the array, use the push, insert, removeAt or clear methods in FormArray itself.
建議使用push,insert,removeAt來操作FormArray裡面的controls

移除FormArray裡面的control

  • removeAt(index: number): void
    要傳1個index
  • push(control: AbstractControl): void
    push一個control到尾端
  • insert(index: number, control: AbstractControl): void
    ^^^^^插入的位置
  • reset(value: any = [], options: { onlySelf?: boolean; emitEvent?: boolean; } = {}): void

Resets the FormArray and all descendants are marked pristine and untouched, and the value of all descendants to null or null maps.

  1. controls的值會變成null或設定的值
  2. controls的狀態會恢復成pristine、untouched
  3. 如果FormArray是放FormGroup,裡面又放FormGroup。則是會呼叫後代的reset
練習用資料
formData = new FormGroup({
    name: new FormControl(),
    items: new FormArray([
        new FormGroup({ address: new FormControl() }),
        new FormGroup({ address: new FormControl() }),
        new FormGroup({ address: new FormControl() })
    ])
})
  • setValue(value: any[], options: { onlySelf?: boolean; emitEvent?: boolean; } = {}): void
    ^^^ 設定值 ^^^^ cd要不要被觸發到
    emitEvent使用情境:當FormControl有綁定valueChange事件,我們用setValue不希望觸發到valueChange事件時
    會針對整個狀態的值做改變
  • patchValue(value: any[], options: { onlySelf?: boolean; emitEvent?: boolean; } = {}): void
    可只改某部分值
  • getRawValue(): any[]
    當control是disabled時,可用getRawValue()取值

updateOn property 什麼時間點要更新值

https://angular.io/api/forms/FormArray#set-the-updateon-property-for-all-controls-in-a-form-array
預設是改變資料 就會更新control的值

const arr = new FormArray([
   new FormControl()
], {updateOn: 'blur'}); // 輸入的當下值不會改變,直到離開時才會更新
                           減少model change的頻率(不用RxJS)

可以用FormBuilder建構FormArray

https://angular.io/api/forms/FormBuilder

監聽FormArray、FormGroup、FormControl的值的變化valueChanges

https://angular.io/api/forms/AbstractControl#valueChanges
valueChanges: Observable<any>

A multicasting observable that emits an event every time the value of the control changes, in the UI or programmatically.

app.component.ts

import { Component } from '@angular/core';
import { FormGroup, FormControl, FormArray, FormBuilder } from '@angular/forms';

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnChanges{
    constructor(private fb: FormBuilder){
        // 由於Reactive Form跟model的資料是同步,所以可以寫在constructor
        this.formData.get('name').valueChanges.subscribe(console.log);
                                                          ^^^^^當name改變時觸發
        this.formData.get('name').valueChanges.pipe().subscribe(console.log); 
                                               ^^^^ 可以用RxJS整合處理多個欄位的值
    }

    formData = new FormGroup({
        name: new FormControl('test value changes'),  // <----- 若這裡給預設值不會觸發
              // 在ngOnInit()或ngOnChanges()設預設值就會觸發constructor()裡的監聽
        items: new FormArray([
            new FormGroup({ address: new FormControl() }),
            new FormGroup({ address: new FormControl() }),
            new FormGroup({ address: new FormControl() })
        ])
    })
    formData2 = this.fb.group({
        items: this.fb.array([
            // 一樣開始放 FormGroup 或 FormControl
        ], { updateOn: 'blur' }) // 也是有updateOn
    });
}
constructor(private fb: FormBuilder){

AbstractControl 的 updateValueAndValidity()

重新計算control的value跟validation status
https://angular.io/api/forms/AbstractControl#updateValueAndValidity
使用情境:
預設會連帶更新上層(ancestors)的value跟validity
當進驗手動驗證時(當關掉自動驗證時),要自己更新model的value

updateValueAndValidity(opts: { onlySelf?: boolean; emitEvent?: boolean; } = {}): void

  • onlySelf
  1. ture 只更新此control
  2. (預設)false會影響ancestors
  • emitEvent: 當control有變更時
  1. true:會觸發statusChanges跟valueChanges observable,且含最新的status跟value
  2. (預設)false 不會emit event,

FormArray的setValue跟patchValue

// 假設FormArray裡有3個FormControl
    formData = new FormGroup({
        name: new FormControl(),
        items: new FormArray([
            new FormGroup({ address: new FormControl() }),
            new FormGroup({ address: new FormControl() }),
            new FormGroup({ address: new FormControl() })
        ])
    });
    
reset(){
    this.items.reset([
        { address: '1' },
        { address: '2' },
        { address: '3' }
    ]);
    
    console.log(this.items.value); // 此FormArray的所有值
    let newValue = this.items.value.slice();
    newValue[1].address = '5'; // 更新第2個control的address
    this.items.reset(newValue);
}
  • 動態更新整個FormArray
editData = {
    name: 'Kevin',
    items: [
        {address:1}, // new FormGroup({ address: new FormControl() }),
        {address:2},
        {address:3}
    ]
};

get items(){
    return this.formData.get('items') as FormArray;
}
// 要更新的model
formData = this.fb.group({ // private fb: FormBuilder
    name: '',
    items: this.fb.array([]) // 空的FormArray
});

ngOnInit() {
    this.formData.reset(); // 都填null
    // 寫法一:
    1. 建表單架構
    this.editData.items.forEach(item => { // 走訪this.editData.items,塞到formData.items
        // 應該類似加,讓結構一樣 new FormGroup({ address: new FormControl() }),
        this.items.push(this.fb.group(item));
        ^^^^^^^^^^ this.formData.get('items')
    });
    2.更新值
    this.formData.patchValue(this.editData); // form的結構一樣的,更新值
    
    // 寫法二: 用reset
    1. 先清空
    while(this.items.length >1){
        this.items.removeAt(1);
    }
    2. 建表單架構
    this.editData.items.forEach(item => {
        this.items.push(this.fb.group( { address:null} ));
    });
    3. 更新值
    this.formData.reset(this.editData); // form的結構一樣的,更新值

對於FormArray,其他不對的寫法

如果是該items有掛Observable監聽 的時候
this.formData.get('items') = new this.fb.array();
                                ^^^^^^^^^^ 指到新物件,但舊的物件還活著,監聽都還在
沒有比較好的方法,把對FormArray的監聽都移除
(所以要透過push,insert,removeAt models來操作FormArray)

上一篇
Day08_Form Part I - Template-Driven Form
下一篇
Day10_動態產生元件及應用
系列文
Angular新手村學習筆記(2019)33
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言